昨天我們為我們的 API 加上文件了,今天讓我們來實作分頁這個功能吧!
先說說什麼是分頁,分頁的意思是當使用者請求列表的 API 時,一次只給他一部份並讓他可以透過指定範圍或是頁碼的方式去選取他要哪個部分的資料。會需要這個功能的原因是因為當資料庫中存在大量的資料時,如果將它全部列出給請求者,會造成資料庫以及網路傳輸很大的負擔,所以我們一次只給使用者一部分,如果他有需要就可以再請求下個部分的資料。
感謝方便的 DRF 他提供了已經實作好的分頁器可以使用,讓我們把它加到我們既有的 ViewSet 中,打開 server/app/todo/views.py
並依照下方內容修改
-from rest_framework import decorators, response, viewsets
+from rest_framework import decorators, pagination, response, viewsets
from server.app.todo import models as todo_models
from server.app.todo import serializers as todo_serializers
class TaskViewSet(viewsets.ModelViewSet):
queryset = todo_models.Task.objects.all()
serializer_class = todo_serializers.TaskSerializer
+ pagination_class = pagination.LimitOffsetPagination
# ...... 以下省略 ......
同時打開 server/settings.py
並修改預設分頁大小設定
# ...... 以上省略 ......
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
+ "PAGE_SIZE": 10,
}
# ...... 以下省略 ......
這時候大家打開 Postman 並使用 GET 方法請求 http://127.0.0.1:8000/api/todo/tasks(記得啟動虛擬環境 & server 唷)應該會看到類似這樣的請求
{
"count": 22,
"next": "http://127.0.0.1:8000/api/todo/tasks?page=2",
"previous": null,
"results": [
{
"id": 5,
"tags": [
{
"id": 1,
"name": "T01"
}
],
"title": "測試任務一",
"description": "這是一個測試任務",
"is_finish": false
},
"....... 這邊會是 Task 資料,以下就省略了 ......."
]
}
到這邊我們就可以發現我們這次的請求只拿回了前十筆的資料,如果我們想要第二頁的資料那我們就必須要請求 http://127.0.0.1:8000/api/todo/tasks?page=2 這樣就可以得到再後面十筆的資料了。
現在我們已經在 ViewSet 中加入分頁器了,但與設定權限時的邏輯相同這邊只針對當前的 ViewSet 套用了設定,我們現在一樣將分頁的設定套用到全部的 ViewSet 中,讓我們打開 server/app/todo/views.py
並依照下方內容修改
-from rest_framework import decorators, pagination, response, viewsets
+from rest_framework import decorators, response, viewsets
from server.app.todo import models as todo_models
from server.app.todo import serializers as todo_serializers
class TaskViewSet(viewsets.ModelViewSet):
queryset = todo_models.Task.objects.all()
serializer_class = todo_serializers.TaskSerializer
- pagination_class = pagination.LimitOffsetPagination
# ...... 以下省略 ......
我們先把針對單一 ViewSet 套用的設定拿掉,接著打開 server/settings.py
並依照下方內容修改
# ...... 以上省略 ......
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
+ "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
# ...... 以下省略 ......
這樣我們再試著請求看看,應該每個列表 API 都已經套用分頁了。
在分頁都加完成後大家可以到啟動 server 的終端機應該會看到類似這樣的警告訊息
UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'server.app.todo.models.Task'> QuerySet.
paginator = self.django_paginator_class(queryset, page_size)
會出現這樣的原因是因為我們的 queryset 並沒有進行排序,但是如果要實作分頁排序就是必須的。原因是因為我們請求列表時第一次他會給我資料的地 1 ~ 10 筆,當我請求 page=2
時他會給我 11 ~ 20 筆,那這時如果我們的排序是亂的就有可能造成每次出來的結果不正確,所以在做分頁前需要先排序。
接著讓我們打開 server/app/task/views.py
依照下方內容進行修改
# ...... 以上省略 ......
class TaskViewSet(viewsets.ModelViewSet):
- queryset = todo_models.Task.objects.all()
+ queryset = todo_models.Task.objects.order_by("id")
serializer_class = todo_serializers.TaskSerializer
def get_serializer_class(self):
if self.action == "create":
return todo_serializers.TaskCreateSerializer
return super().get_serializer_class()
@decorators.action(methods=["patch"], detail=True)
def status(self, request, pk):
task = self.get_object()
serializer = self.get_serializer(
task,
data={"is_finish": not task.is_finish},
partial=True,
)
serializer.is_valid(raise_exception=True)
serializer.save()
return response.Response(serializer.data)
class TagViewSet(viewsets.ModelViewSet):
- queryset = todo_models.Tag.objects.all()
+ queryset = todo_models.Tag.objects.order_by("id")
serializer_class = todo_serializers.TagSerializer
到這邊我們再次請求一次我們的列表 API 應該會看到剛剛的警告不見了。
P.S. 這邊補充說明一下,有些人會習慣在寫完 order_by
後還是會加上 all
雖然不會有錯誤產生,但其實是不必要的所以可以省略,看起來會簡潔些。
使用到這邊雖然用頁碼的分頁已經可以正常的使用了,但是我們的分頁大小是在設定中寫死的,不能依照前端的需求自行調整,為了滿足這個功能我們需要客製化一下分頁器。
讓我們先建立資料夾 server/utils
並建立檔案 server/utils/__init__.py
和 server/utils/pagination.py
,如果不想手動建立可以執行下方指令
mkdir server/utils # 建立資料夾
touch server/utils/__init__.py # 建立檔案
touch server/utils/pagination.py # 建立檔案
並在 server/utils/pagination.py
裡面貼入下方內容
from rest_framework import pagination
class PageNumberPagination(pagination.PageNumberPagination):
page_size_query_param = "page_size"
max_page_size = 1000
這邊我們做的事是建立一個 PageNumberPagination 他繼承了 DRF 的 PageNumberPagination 並設定我們可以使用 page_size
這個 query params 來指定分頁的大小,並設定最大的大小不能超過 1000。
接著打開 server/settings.py
並修改
# ...... 以上省略 ......
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
- "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
+ "DEFAULT_PAGINATION_CLASS": "server.utils.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
# ...... 以下省略 ......
上方的修改是告訴 DRF 預設的分頁器要使用我們自己定義的那個分頁器。
這時候我們可以打開 Postman 並使用 GET 請求這個網址 http://127.0.0.1:8000/api/todo/tasks?page_size=3 大家會發現我們的 API 只回傳了 3 筆資料,這樣就完成客製化了。
除了我們今天示範的使用頁碼的分頁器以外 DRF 其實還提供了其他分頁方法,不過設定的方式都相同唯一的差別是回傳給前端的格式,大家可以依照需求使用,如果有需要可以參考官方文件。
今天我們為我們的 API 設定分頁了,他可以避免後面資料變多後造成傳輸與資料庫的負擔。
結束前別忘了檢查一下今天的程式碼有沒有問題,並排版好喔。
ruff check --fix .
black .
pyright .
今天的內容就到這邊了,讓我們期待明天的內容吧。
P.S. 今天的檔案更新可以參考我的 Git Commit 大家可以搭配服用